package net.kucoe.grin.context.application.xml;

import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.XMLConstants;
import javax.xml.parsers.*;
import javax.xml.transform.Source;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import net.kucoe.grin.Grin;
import net.kucoe.grin.context.application.*;

import org.w3c.dom.*;
import org.xml.sax.*;
import org.xml.sax.helpers.XMLFilterImpl;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * Reads configuration from xml.
 * 
 * @author Vitaliy Basyuk
 */
public class ConfigurationProcessor {
    
    private static final Logger logger = Grin.getLogger("ConfigurationProcessor");
    
    @SuppressWarnings("unused")
    private static final String APPLICATION = "application";
    private static final String INIT_SYSTEM = "init-system";
    private static final String INIT_RESULT = "init-result";
    private static final String ARGUMENT = "argument";
    private static final String NAME = "name";
    private static final String VALUE = "value";
    @SuppressWarnings("unused")
    private static final String REF = "ref";
    private static final String CLASS = "class";
    
    /**
     * Reads configuration.
     * 
     * @param filename string
     * @param validate
     * @return {@link GrinApplication}
     * @throws ClassNotFoundException
     */
    public GrinApplication processFile(final String filename, final boolean validate) throws ClassNotFoundException {
        GrinApplication configuration = new GrinApplication();
        logger.config("Start reading configuration from file:" + filename);
        if (validate) {
            validate(filename);
        }
        Document document = parseFile(filename);
        List<InitSystem> initSystems = readInitSystems(document);
        configuration.setInitSystems(initSystems);
        logger.config("Configuration read from file:" + filename);
        return configuration;
    }
    
    private List<InitSystem> readInitSystems(final Document document) throws ClassNotFoundException {
        Element root = document.getDocumentElement();
        NodeList list = root.getElementsByTagName(INIT_SYSTEM);
        List<InitSystem> initSystems = new LinkedList<InitSystem>();
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);
            if (node instanceof Element) {
                Element element = (Element) node;
                String attribute = element.getAttribute(CLASS);
                InitSystem initSystem = new InitSystem();
                initSystem.setClazzName(attribute);
                element = (Element) element.getElementsByTagName(INIT_RESULT).item(0);
                attribute = element.getAttribute(CLASS);
                InitResult initResult = new InitResult();
                initResult.setClazzName(attribute);
                initSystem.setInitResult(initResult);
                NodeList nodes = element.getElementsByTagName(ARGUMENT);
                List<InitArgument> arguments = new LinkedList<InitArgument>();
                for (int j = 0; j < nodes.getLength(); j++) {
                    node = nodes.item(j);
                    if (node instanceof Element) {
                        Element argumentEl = (Element) node;
                        String name = argumentEl.getAttribute(NAME);
                        String value = argumentEl.getAttribute(VALUE);
                        InitArgument argument = new InitArgument();
                        argument.setName(name);
                        argument.setValue(value);
                        arguments.add(argument);
                    }
                }
                initResult.setInitArguments(arguments);
                initSystems.add(initSystem);
            }
        }
        return initSystems;
    }
    
    private void validate(final String file) {
        InputSource inputSource = new InputSource(new StringReader(file));
        try {
            XMLReader xmlReader = XMLReaderFactory.createXMLReader();
            XMLFilter defaultNamespaceFilter =
                    new DefaultNamespaceFilter(xmlReader, "http://www.kucoe.net/schema/grin");
            SAXSource saxSource = new SAXSource(defaultNamespaceFilter, inputSource);
            javax.xml.validation.Schema xsdSchema = loadXsdSchema();
            Validator validator = xsdSchema.newValidator();
            validator.validate(saxSource);
        } catch (SAXException e) {
            logger.log(Level.SEVERE, "Error while parsing file: " + file, e);
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Error while parsing file: " + file, e);
        }
    }
    
    private javax.xml.validation.Schema loadXsdSchema() throws SAXException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        InputStream xsdStream = getClass().getClassLoader().getResourceAsStream("grin.xsd");
        if (xsdStream == null) {
            throw new RuntimeException("Could not load schema XSD from grin.xsd");
        }
        try {
            Source xsdSource = new StreamSource(xsdStream);
            return factory.newSchema(xsdSource);
        } finally {
            try {
                xsdStream.close();
            } catch (IOException e) {
                // skip
            }
        }
    }
    
    private Document parseFile(final String file) {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            factory.setNamespaceAware(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(file);
        } catch (SAXException e) {
            logger.log(Level.SEVERE, "Error while parsing file: " + file, e);
        } catch (IOException e) {
            logger.log(Level.SEVERE, "Error while parsing file: " + file, e);
        } catch (ParserConfigurationException e) {
            logger.log(Level.SEVERE, "Error while parsing file: " + file, e);
        }
        return null;
    }
    
    private static final class DefaultNamespaceFilter extends XMLFilterImpl {
        
        private final String defaultNamespace;
        
        public DefaultNamespaceFilter(final XMLReader parent, final String defaultNamespace) {
            super(parent);
            this.defaultNamespace = defaultNamespace;
        }
        
        @Override
        public void startElement(final String uri, final String localName, final String qName, final Attributes atts)
                throws SAXException {
            String resultUri = uri;
            if (uri == null || uri.isEmpty()) {
                resultUri = defaultNamespace;
            }
            super.startElement(resultUri, localName, qName, atts);
        }
        
    }
}
